package com.lyb.hardware.disk;

import com.lyb.Utils;
import com.lyb.constant.DiskConstant;
import com.lyb.constant.SystemConstant;
import com.lyb.hardware.AlpacaMainBoard;

import java.io.IOException;
import java.nio.ByteBuffer;
import java.util.Scanner;

/**
 * 文件类
 *
 * @author Mr.Alpaca potato
 * @version 1.4.0
 */
public class AlpacaFile {
    /**
     * 二进制格式文件内容
     */
    private byte[] content;
    /**
     * 文件目录项
     */
    private DirectoryEntry directoryEntry;
    /**
     * 主板
     */
    private final AlpacaMainBoard mainBoard;
    /**
     * 文件路径
     */
    private String filePath;
    /**
     * 父目录保存的目录项的磁盘号
     */
    private byte fatherDiskNum;


    public AlpacaFile() {
        /* 获取主板对象，通过主板提供的磁盘API对磁盘进行操作 */
        this.mainBoard = SystemConstant.MAIN_BOARD;
        this.directoryEntry = new DirectoryEntry();
    }

    /**
     * 根据文件路径构造对象
     *
     * @param filePath 文件路径
     */
    public AlpacaFile(String filePath) {
        this();
        this.filePath = filePath;
        /* 解析路径，将文件名字和类型赋予目录项 */
        parseFilePath();
    }

    /**
     * 解析文件路径，根据文件名和类型创建目录项
     */
    private void parseFilePath() {
        /* 先获取最后一级路径 */
        String replace = this.filePath.replace("\\", "/");
        String[] split = replace.split("/");
        /* 将文件名以分隔符分割为名字和类型 */
        String[] file = split[split.length - 1].split("\\.");
        this.directoryEntry = new DirectoryEntry(file[0], file[1]);
    }

    /**
     * 读取屏幕输入，将屏幕输入转换为二进制并赋予对象属性
     */
    public void screenInput() {
        StringBuilder screenInput = new StringBuilder();
        Scanner scanner = new Scanner(System.in);
        String input;
        while ((input = scanner.nextLine()) != null) {
            /* 遇到 "EOF" 结束文件输入 */
            if (SystemConstant.FILE_END.equals(input)) {
                break;
            }
            input += "\n";
            screenInput.append(input);
        }
        /* 将屏幕输入赋予属性值 */
        setContent(screenInput.toString().getBytes());
        this.directoryEntry.setLength(this.content.length);
    }

    /**
     * 读取GUI界面输入，将界面输入转换为二进制并赋予对象属性
     */
    public void guiInput() {
        /* 系统设置为读取文件内容状态 */
        Utils.setCanFileFlag(true);
        synchronized (Utils.class) {
            try {
                Utils.class.wait();
            } catch (InterruptedException e) {
                Utils.outCommandResult("[系统]: 接收文件内容输入出现异常");
                System.exit(201);
            }
        }
        /* 将GUI文件内容赋予属性值 */
        setContent(Utils.getFileContent().getBytes());
        this.directoryEntry.setLength(this.content.length);
    }


    /* ********** 读 ********** */

    /**
     * 读取文件内容，将存储文件内容的字节数组转换为字符串类型返回
     *
     * @return 字符串类型，文件内容
     */
    public String readContent() {
        return new String(content);
    }

    /**
     * 读取文件，将磁盘内指定磁块号的内容读取出来并赋予属性
     * 返回布尔值
     *
     * @return 操作结果，true表示操作成功
     */
    public boolean readFromDisk() {
        /* 判断文件是否存在于磁盘中 */
        if (!exists()) {
            Utils.outCommandResult("[系统|磁盘]: 文件不存在");
            return false;
        }

        try {
            /* 调用主板提供的磁盘API，获取磁块全部内容(若磁块未被填满也会返回空字节) */
            String originContent = new String(mainBoard.readBlocksFromDisk(directoryEntry.getStartDiskNum()));
            int lastIndex = originContent.lastIndexOf("\n");
            /* 只读取磁块中有意义的部分(去除用于填充的空字节) */
            this.content = originContent.substring(0, lastIndex + 1).getBytes();
            this.directoryEntry.setLength(this.content.length);
            return true;
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 文件读取时发生异常！！！");
        }
        return false;
    }

    /**
     * 查找文件是否存在于磁盘中，返回布尔值
     *
     * @return 布尔值，true表示文件存在于磁盘中
     */
    public boolean exists() {
        this.filePath = this.filePath.replace("\\", "/");
        String[] filePath = this.filePath.split("/");

        /* 从根目录开始寻找 */
        AlpacaDirectory directory = new AlpacaDirectory(mainBoard.getRootDirectoryFromDisk());
        DirectoryEntry entry = null;
        /* 文件名所在索引 */
        int lastIndex = filePath.length - 1;
        for (int i = 0; i < lastIndex; i++) {
            entry = directory.findDirectoryEntry(filePath[i], " ");
            if (entry == null) {
                return false;
            }

            try {
                /* 将该目录磁块读出作为下次遍历的起始目录 */
                directory = new AlpacaDirectory(mainBoard.readBlockFromDisk(entry.getStartDiskNum()));
            } catch (IOException e) {
                Utils.outCommandResult("[系统|磁盘]: 目录读取失败！！！");
                return false;
            }
        }

        /*
            将文件父目录赋予起始磁盘号保存，用于写入磁盘时往父目录添加目录项
            注意：entry必不为null
         */
        assert entry != null;
        this.fatherDiskNum = entry.getStartDiskNum();

        /* 获取文件名和文件类型 */
        String[] split = filePath[lastIndex].split("\\.");
        /* 查找该文件目录项有无存在于磁盘中 */
        entry = directory.findDirectoryEntry(split[0], split[1]);
        if (entry != null) {
            /* 将查找到目录项赋予当前对象目录项 */
            this.directoryEntry = entry;
            return true;
        }
        return false;
    }


    /* ********** 写 ********** */

    /**
     * 写入磁盘，将文件内容写入磁盘，返回布尔值
     *
     * @return 布尔值，true表示写入成功
     */
    public boolean writeToDisk() {
        /* 若文件存在于磁盘中则先清空原先内容，清空失败则写入也失败 */
        if (exists()) {
            return delete();
        }

        /* 该文件的起始磁盘号 */
        byte startDiskNum = -1;
        try {
            /* 调用磁盘API */
            startDiskNum = mainBoard.writeBlocksToDisk(this.content);
            /* 若返回结果为-1则表示写入失败 */
            if (startDiskNum == -1) {
                Utils.outCommandResult("[系统|磁盘]: 文件写入磁盘失败");
                return false;
            }
            this.directoryEntry.setStartDiskNum(startDiskNum);
            writeToFatherDirectory();
            return true;
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 文件写入磁盘时发生异常！！！");
            /* 若目录项写入失败则将参与操作的磁盘清空 */
            if ("error".equals(e.getMessage())) {
                mainBoard.clearBlocksToDisk(startDiskNum);
            }
        }
        return false;
    }

    /**
     * 将当前文件目录项写入父目录磁块的空部分里
     *
     * @throws IOException 读取磁块出现异常或父目录已填满
     */
    private void writeToFatherDirectory() throws IOException {
        /* 目录项所在磁块在判断文件是否存在时已经获得 */
        byte[] oldContent = mainBoard.readBlockFromDisk(this.fatherDiskNum);
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);

        byte[] subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];

        /* 设置标志位判断新目录项是否已经写入 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(subDirectoryEntry);

            /* 若新目录项还没有写入且读出二进制内容起始磁盘号为0（说明该部分没有内容），则将新目录项写入原数组 */
            if (!operationFlag && subDirectoryEntry[7] == 0) {
                this.directoryEntry.setDirectoryEntry();
                subDirectoryEntry = this.directoryEntry.getDirectoryEntry();
                operationFlag = true;
            }

            newBuffer.put(subDirectoryEntry);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 如果新目录项没有执行写入磁块，则说明磁块已经满了 */
        if (!operationFlag) {
            Utils.outCommandResult("[系统|磁盘]: 父目录已填满，目录写入失败");
            throw new IOException("error");
        }

        /* 将目录项内容写回磁盘 */
        mainBoard.writeBlockToDisk(this.fatherDiskNum, newBuffer.array());
    }

    /**
     * 将修改后的文件目录项写回父目录磁块里
     *
     * @throws IOException 读取磁块失败
     */
    public void writeDirectoryBack() throws IOException {
        /* 目录项所在磁块在判断文件是否存在时已经获得 */
        byte[] oldContent = mainBoard.readBlockFromDisk(this.fatherDiskNum);
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);

        /* 一次读取一个目录项的字节数组 */
        byte[] subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];

        /* 设置标志位判断目录项是否已经写回 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(subDirectoryEntry);

            /* 若目录项还没有写回且目录项起始磁盘等于文件起始磁盘（说明找到原目录项），则将新目录项写入原数组 */
            if (!operationFlag && subDirectoryEntry[7] == this.directoryEntry.getStartDiskNum()) {
                subDirectoryEntry = this.directoryEntry.getDirectoryEntry();
                operationFlag = true;
            }

            /* 将目录项写回缓冲 */
            newBuffer.put(subDirectoryEntry);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 将目录项内容写回磁盘 */
        mainBoard.writeBlockToDisk(this.fatherDiskNum, newBuffer.array());
    }

    /**
     * 从磁盘中删除文件，并将目录项从父目录磁块中删除
     * 返回布尔值
     *
     * @return 布尔值，表示操作结果
     */
    public boolean delete() {
        /* 删除之前先判断文件存不存在 */
        if (!exists()) {
            Utils.outCommandResult("[系统|磁盘]: 文件不存在");
            return false;
        }
        /* 删除目录项 */
        try {
            deleteDirectoryEntryInFather();
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 文件目录删除失败");
            return false;
        }
        /* 调用磁盘API */
        return mainBoard.clearBlocksToDisk(this.directoryEntry.getStartDiskNum());
    }

    /**
     * 从父目录中删除该文件目录项
     *
     * @throws IOException 读取父目录磁盘内容失败
     */
    private void deleteDirectoryEntryInFather() throws IOException {
        /* 读取父目录磁盘内容 */
        byte[] oldContent = mainBoard.readBlockFromDisk(this.fatherDiskNum);
        ByteBuffer oldBuffer = ByteBuffer.wrap(oldContent);
        ByteBuffer newBuffer = ByteBuffer.allocate(oldContent.length);
        /* 开辟子目录项缓冲 */
        byte[] subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];

        /* 切分文件路径名 */
        String[] filePath = this.filePath.split("/");
        /* 文件名所在目录 */
        int lastIndex = filePath.length - 1;
        /* 获取文件名和类型 */
        String fileName = filePath[lastIndex].split("\\.")[0];
        String fileType = filePath[lastIndex].split("\\.")[1];
        /* 操作标志 */
        boolean operationFlag = false;

        do {
            /* 一次读取8字节 */
            oldBuffer.get(subDirectoryEntry);

            /* 若目录项已删除了则后续目录项写回即可 */
            if (!operationFlag) {
                DirectoryEntry entry = new DirectoryEntry(subDirectoryEntry);
                /* 若目录项与文件名和类型相同则直接将该目录项清空 */
                if (entry.getName().equals(fileName) && entry.getType().equals(fileType)) {
                    subDirectoryEntry = new byte[DiskConstant.DIRECTORY_ENTRY_LENGTH];
                    operationFlag = true;
                }
            }

            newBuffer.put(subDirectoryEntry);
            /* 若当前缓冲区已经读取完则跳出循环 */
        } while (oldBuffer.position() != oldBuffer.limit());

        /* 将目录项内容写回磁盘 */
        mainBoard.writeBlockToDisk(this.fatherDiskNum, newBuffer.array());
    }

    /**
     * 拷贝文件
     * 将源文件复制到目的目录中，逻辑
     * 1.判断文件存在
     * 2.判断路径存在
     * 3.拼接文件目的路径
     * 4.判断目的目录是否已存在同名文件
     * 5.在目的目录中创建文件(调用文件类 API)
     *
     * @param srcFilePath      源文件路径
     * @param tagDirectoryPath 目的目录路径
     * @return 拷贝结果
     */
    public boolean copy(String srcFilePath, String tagDirectoryPath) {
        /* 1.判断文件是否存在 */
        AlpacaFile file = new AlpacaFile(srcFilePath);
        if (!file.exists()) {
            Utils.outCommandResult("[系统|磁盘]: 文件 " + srcFilePath + " 不存在");
            return false;
        }
        /* 获取文件内容 */
        file.readFromDisk();

        /* 2.判断目录是否存在 */
        AlpacaDirectory directory = new AlpacaDirectory(tagDirectoryPath);
        if (!directory.exists()) {
            Utils.outCommandResult("[系统|磁盘]: 目录 " + tagDirectoryPath + "不存在");
            return false;
        }

        /* 3.拼接文件目的路径 */
        String fileName = srcFilePath.replace("\\", "/").substring(srcFilePath.lastIndexOf("/"));
        String tagFileName = tagDirectoryPath + fileName;
        /* 根据文件路径创建对象 */
        AlpacaFile alpacaFile = new AlpacaFile(tagFileName);

        /* 4.判断目录下是否存在同名文件 */
        if (alpacaFile.exists()) {
            Utils.outCommandResult("[系统|磁盘]: 目录 " + " 下已存在文件 " + fileName);
            return false;
        }

        /* 复制内容 + 目录项确定文件长度 */
        alpacaFile.setContent(file.getContent());
        alpacaFile.getDirectoryEntry().setLength(file.getContent().length);

        /* 5.在指定目录中创建文件(调用文件类 API) */
        return alpacaFile.writeToDisk();
    }

    /**
     * 移动文件到指定目录，逻辑
     * 1.判断文件存在
     * 2.判断目录存在
     * 3.获取文件目录项
     * 4.将文件目录项添加到指定目录磁块下
     * 5.从原目录中删除该文件目录项
     *
     * @param filePath      文件路径
     * @param directoryPath 目录路径
     * @return 移动结果
     */
    public boolean move(String filePath, String directoryPath) {
        /* 1.判断文件是否存在 */
        AlpacaFile file = new AlpacaFile(filePath);
        if (!file.exists()) {
            Utils.outCommandResult("[系统|磁盘]: 文件 " + filePath + " 不存在");
            return false;
        }
        /* 2.判断目录是否存在 */
        AlpacaDirectory directory = new AlpacaDirectory(directoryPath);
        if (!directory.exists()) {
            Utils.outCommandResult("[系统|磁盘]: 目录 " + directoryPath + "不存在");
            return false;
        }
        /* 3.获取文件目录项(在判断文件是否存在时已确定) */
        DirectoryEntry fileEntry = file.getDirectoryEntry();

        /* 3.文件目录项添加到指定目录磁块下 */
        if (!directory.addDirectoryEntry(fileEntry)) {
            Utils.outCommandResult("[系统|磁盘]: 文件目录项移动失败");
            return false;
        }

        /* 4.从原目录中删除该文件目录项 */
        try {
            /* 父目录对象 */
            AlpacaDirectory fatherDirectory = new AlpacaDirectory(mainBoard.readBlockFromDisk(file.fatherDiskNum));
            fatherDirectory.setDiskNum(file.fatherDiskNum);
            /* 删除文件目录项 */
            return fatherDirectory.deleteDirectoryEntry(fileEntry);
        } catch (IOException e) {
            Utils.outCommandResult("[系统|磁盘]: 文件父目录读取失败");
            return false;
        }
    }


    /* ********** Getter & Setter ********** */
    public void setContent(byte[] content) {
        this.content = content;
    }

    public byte[] getContent() {
        return content;
    }

    public DirectoryEntry getDirectoryEntry() {
        return directoryEntry;
    }

    public String getFilePath() {
        return filePath;
    }
}
